package com.idanchuang.component.redis.task;

import com.idanchuang.component.core.ServiceInfo;
import com.idanchuang.component.core.util.InetUtil;
import com.idanchuang.component.logback.metric.MetricLogger;
import com.idanchuang.component.redis.util.RedisUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 分布式定时任务抽象实现 (基于Redis lua脚本进行执行权抢占与持有)
 *
 * @author yjy
 * Created at 2022/1/20 10:03 上午
 */
public abstract class AbstractTimedTask implements TimedTask {

    private static final Logger logger = LoggerFactory.getLogger(TimedTask.class);

    /**
     * 本机ip
     */
    protected static final String LOCAL_IP = InetUtil.getLocalIp();
    /**
     * jvm进程id
     */
    protected static final String PROCESSOR_ID = ManagementFactory.getRuntimeMXBean().getName().split("@")[0];

    /**
     * 任务执行调度线程
     */
    protected final ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1,
            (r) -> new Thread(r, "redis-timed-task"));

    /**
     * 开始任务
     */
    @Override
    public void start() {
        Map<String, Object> metaInfo = new HashMap<>(8);
        metaInfo.put("taskName", this.getTaskName());
        scheduler.scheduleAtFixedRate(() -> {
            try {
                if (tryRequire()) {
                    long start = System.currentTimeMillis();
                    try {
                        this.runOnce();
                        metaInfo.put("success", true);
                        metaInfo.put("error", "");
                    } catch (Throwable e) {
                        metaInfo.put("success", false);
                        metaInfo.put("error", e.getMessage());
                        throw e;
                    } finally {
                        long cost = System.currentTimeMillis() - start;
                        MetricLogger.log("RedisTimedTask", cost, metaInfo);
                    }
                }
            } catch (Exception e) {
                logger.error("TimedTask error", e);
            }
        }, getInitialDelay(), getInterval(), getTimeUnit());
        logger.info("RedisTimedTask running >>> {}", this.getTaskName());
    }

    @Override
    public String getTaskName() {
        return this.getClass().getName();
    }

    @Override
    public TimeUnit getTimeUnit() {
        return TimeUnit.SECONDS;
    }

    @Override
    public long getInitialDelay() {
        return 0L;
    }

    @Override
    public void destroy() {
        logger.info("TimeTask {} destroy...", getTaskName());
        // 停止调度线程
        scheduler.shutdownNow();
        // 释放占有的redis key
        tryRelease();
    }

    /**
     * 检查/续命执行权
     * @return ..
     */
    private boolean tryRequire() {
        String key = getKey();
        String instanceId = LOCAL_IP + ":" + PROCESSOR_ID;
        long interval = getTimeUnit().toSeconds(getInterval());
        long ttl = (long) (interval * 1.5D);
        if (interval == ttl) {
            // 至少多占1s
            ttl = interval + 1;
        }
        String script = "local instanceId,ttl=ARGV[1],ARGV[2] ";
        script += "if redis.call('EXISTS', KEYS[1])==1 " +
                "then local curValue = redis.call('GET', KEYS[1]) " +
                "if curValue==instanceId " +
                "then redis.call('EXPIRE', KEYS[1], ttl) " +
                "return true " +
                "else " +
                "return false " +
                "end " +
                "else " +
                "redis.call('SET', KEYS[1], instanceId) " +
                "redis.call('EXPIRE', KEYS[1], ttl) " +
                "return true " +
                "end";
        DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(Boolean.class);
        redisScript.setScriptText(script);
        List<String> keys = new ArrayList<>();
        keys.add(key);
        return RedisUtil.getInstance().execute(redisScript, keys, instanceId, String.valueOf(ttl));
    }

    /**
     * 释放执行权
     */
    private void tryRelease() {
        String key = getKey();
        String instanceId = LOCAL_IP + ":" + PROCESSOR_ID;
        String script = "local instanceId=ARGV[1] ";
        script += "if redis.call('EXISTS', KEYS[1])==1 " +
                "then local curValue = redis.call('GET', KEYS[1]) " +
                "if curValue==instanceId " +
                "then redis.call('DEL', KEYS[1]) " +
                "return true " +
                "else return false " +
                "end " +
                "else return false " +
                "end";
        DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(Boolean.class);
        redisScript.setScriptText(script);
        List<String> keys = new ArrayList<>();
        keys.add(key);
        RedisUtil.getInstance().execute(redisScript, keys, instanceId);
    }

    /**
     * 任务对应的redis key
     * @return ..
     */
    protected String getKey() {
        return "TimedTask:" + ServiceInfo.getAppId() + ":" + getTaskName();
    }

}
